这一章名为表达式,其实讲了C++中的一些运算符。大部分其实跟其他编程语言都很类似,在这里把C++11标准的一些特性以及之前不是特别清楚的记录一下。
基本概念
重载运算符
当运算符作用于类类型的运算对象时,用户可以自行定义其含义。因为这种自定义的过程事实上是为已存在的运算符赋予了营外一层含义,所以称之为重载运算符。IO库的>>和<<运算符以及string对象、vector对象和迭代器使用的运算符都是重载运算符。
左值和右值
这两个名词虽然是由C语言中继承过来,但是由于常量对象的存在等原因,不再那么简单。简单归纳为:当一个对象被用作右值的时候,用的是对象的值(内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
优先率和结合律
如果可以尽量加括号
求值顺序
某些运算符没有指定执行顺序,如果表达式中指向并修改了同一个对象,将会导致错误并产生未定义行为。比如<< 运算符没有明确规定何时以及如何对运算对象求值,所以下述代码是未定义的。
1 | int i = 0; |
有四种运算符明确规定了运算对象的求值顺序,即”&&(逻辑与)“,”||(逻辑或)“,” ? :(三目条件运算符) “以及”,(逗号运算符)”
经验:所以如果拿不准尽量使用括号来强制组合关系。如果改变了某个运算对象的值,在表达式的其他地方尽量不要使用这个表达对象。
算术运算符
主要是除法和取余(%)运算,c++11标准规定商一律向0取整,即直接删除小数部分
对于取余运行算,如果m和n是整数且n非0,则表达式(m/n)*n+m%n的求值和m相同.隐含的意思是,如果m%n不等于0,则它的符号和m相同.
逻辑和关系运算符
逻辑与和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。这种策略称为短路求值(short-circuit evaluation)
- 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值
- 对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值
赋值运算符
赋值运算符满足右结合律,如:
1 | int ival, jval; |
但是只能在两种变量类型相同或者允许转换时如此使用。
递增和递减运算符
递增递减运算符有两种形式:前置版本和后置版本
- 前置版本:运算符首先将运算对象进行+1或-1操作,然后将改变后的对象作为求值结果
- 后置版本:运算符也会将运算对象进行+1或-1操作,但是求值结果是运算对象改变之前的那个值的副本。
除非必要,建议不要用后置版本:
因为前置版本的递增递减运算符避免了不必要的工作,它把值+1或-1后直接返回了改变的运算对象。与之相比,后置版本需要将原始值存储下来以便于返回这个未修改的内容。如果我们并不需要修改前的值,那么后置版本的操作就是一种浪费。对于整数或指针类型,这种额外工作编译器可以给出优化,但是对于复杂的迭代器类型,这种额外的工作开销就大了。
在一条语句中混用解引用和递增运算符
我们在一条复合表达式种既将变量+1或-1,又能使用他原来的值,这就是后置版本的使用情景。
注意后置递增运算符的优先级高于解引用运算符。如例子:
1 | auto pbeg = v.begin(); |
这里面的pbeg++ 等同于 (pbeg++)。解释就是pbeg++将指针pbeg的值+1,然后返回pbeg指针的原始值的副本作为求值结果,然后对这个副本指针进行解引用。最终这条语句输出的时pbeg开始时指向的元素的值,同时将指针向后移动一个位置。
运算对象可按任意顺序求值
由于大多数运算符没有规定运算对象的求值顺序,所以如果一条子表达式改变了某个运算对象的值,另一个子表达式又要使用该值的话,运算对象的求值顺序就很关键了。因为递增运算符和递减运算符会改变运算对象的值,所以要提放在复合表达式中错用两个运算符。
成员访问运算符
箭头运算符作用于一个指针类型的运算对象,结果是一个左值。
点运算符分为两种情况:
- 如果成员所属的对象是左值,则结果为左值
- 如果成员所属的对象是右值,则结果为右值
条件运算符
嵌套条件运算符
条件运算符满足右结合律,意味着运算对象一般按照从右向左的顺序组合,所以对于下述代码,靠右的条件运算构成了靠左条件运算的:分支
1 | finalgrade = (grade>90) ? "high pass" : (grade<60) ? "fail" : "pass"; |
在输出表达式中使用条件运算符
条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。
1 | cout << ((grade < 60) ? "fail" : "pass"); //输出fail或者pass |
在第二条表达式中,grade和60的比较结果是<<运算符的运算对象,如果grade<60为真输出1,否则为0。然后<<运算符的返回值是cout,然后cout会作为条件运算符的条件。换言之,第二条语句等价于:
1 | cout << (grade < 60); //输出0 或者 1 |
第三条表达式等价于:
1 | cout << grade ; // 小于运算符的优先级低于移位运算符,所以先输出grade |
位运算符
位运算符作用于整数类型的运算对象,并把运算对象看成是二进制位的集合。
一般来说,如果运算对象是”小整型“,它的值会被自动提升到较大的整数类型。
移位运算符
右侧的运算对象一定不能为负,而且值必须严格小于结果的位数,否则就会出现未定义行为。
左移运算符(<<)在右侧插入值为0的二进制位。右移运算符(>>)的行为则依赖于其左侧运算对象的类型:如果该运算对象是无符号类型,在左侧插入值为0的二进制位;如果该运算对象是带符号类型,在左侧插入符号位的副本或值位0的二进制位,如何选择要视具体环境而定。
位与、位或、位异或运算符
对于位异或运算符(^)来说,如果两个运算对象的对应位置有且只有一个为1,则运算结果中该位位1,否则为0。
使用位运算符
1 | quiz1 |= 1UL << 27; //在不改变其他位的前提下将第27位置为1 |
sizeof运算符
sizeof运算符返回一个表达式或者一个类型名字所占的字节数。sizeof运算符满足右结合律,得到的是一个size_t类型的常量表达式。运算符的运算对象有两种形式:
1 | sizeof (type) |
在第二种形式中,sizeof返回的是表达式的结果类型的大小。注意sizeof并不实际计算其运算对象的值。
表达式sizeof *p
比较特殊,特殊在两个方面
- 由于sizeof满足右结合律并且和运算符优先级一样,所以等价于```sizeof (p)```。
- 因为sizeof不会实际求运算对象的值,所以即使p是一个无效的指针也不会有什么影响。也就是说在sizeof的运算对象中解引用一个无效指针依旧是一个安全行为。因为指针实际上并没有被解引用,sizeof不需要真正的解引用就可以知道他所指的对象的类型。
sizeof运算符的结果部分依赖于其作用的类型,比较特殊的是以下几个:
- 对引用类型执行sizeof运算得到是被引用对象所占空间的大小。
- 对指针执行sizeof运算得到指针本身所占空间的大小
- 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有元素各执行一次运算并将结果求和。注意,sizeof运算不会把数组转换成指针来处理
- 对string对象和vector对象执行sizeof运算只返回该类型固定部分的大小,而不会计算对象中的元素占用了多少空间。。
因此我们可以用数组大小除以单个数组元素的大小得到元素的个数
1 | constexpr size_t sz = sizeof(ia)/sizeof(*ia); |
类型转换
算术类型之间的隐式转换被设计得尽可能避免损失精度。很多时候,如果表达式中既有整数类型的运算对象也有浮点数类型的运算对象,整型会转换成浮点型。
何时发生隐式类型转换
- 大多数表达式中,比int类型小的整型值首先提升为较大的整数类型
- 在条件中,非布尔值会被转换成布尔类型
- 初始化过程中,初始值转换成变量的类型;在赋值语句中,右侧运算对象转换成左侧运算对象的类型。
- 如果算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型
- 函数调用也会发生类型转换
算术转换
整型提升
对于bool、char、signed char、unsigned char、short和unsigned short等类型来说,只要它们所有可能的值都存在int里,他们就会提升成int类型;否则,提升成unsigned int类型。
较大的char类型(wchar_t、char16_t、char32_t)提升成int、unsigned int、long、unsigned long、long long和unsigned long long中最小的一种类型,前提是转换后的类型要能容纳原类型所有可能的值。
无符号类型的运算对象
如果一个运算对象是无符号的,另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算转换成无符号的。
如果带符号类型大于无符号类型,此时转换的结果依赖于机器。
其他隐式类型转换
数组转换成指针
大多数用到数组的表达式,数组可以自动转换成指向数组首元素的指针
但是当数组被用作decltype关键字的参数,或者作为取地址符(&)、sizeof以及typeid等运算符的运算对象时,上述转换不会发生。同样的,如果用一个引用来初始化数组,上述转换也不会发生。
指针的转换
c++规定了几种指针转换方式,包括
- 常量整数值0或者字面值nullptr可以转换成任意指针类型
- 指向任意非常量的指针能转换成void*
- 指向任意对象的指针能转换成const void*
- 具有继承关系的类型间还有另外的指针转换方式
转换成布尔类型
一种从算术类型或指针类型向布尔类型自动转换的机制。如果指针或算术类型的值为0,转换结果为false,否则转换结果为true。
转换为常量
允许将指向非常量类型的指针转换成指向相应的常量类型的指针,对于引用也是这样。但是相反的转换并不存在,因为它试图删掉底层const
类类型定义的转换
类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换。
常见的类类型转换比如:
- 在需要标准库string类型的地方使用C风格字符串
- 在条件部分读入sitream
1 | const char *str = s.c_str(); //s是一个string类型 |
IO库定义了从istream向布尔值的转换规则。所得的布尔值到底是什么由输入流的状态决定,如果最后一次读入成功,转换得到的布尔值为true,相反失败则为false。
显示转换
显示转换即强制类型转换。我在C语言中经常看到的强转都是直接的要转换的类型+expr。c++则有一些规定
命名的强制类型转换
命名的强制类型转换形式是:
cast-name
(expression)
type为转换的目标类型 expression是要转换的值。如果type是引用类型,则结果是左值。cast_name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。cast_name指定了执行的哪种转换。
static_cast ——用于非常量的类型强转
当需要把一个较大的算术类型赋值给较小的类型时,static_cast非常有用。此时强制类型转换告诉程序读者和编译器:我们知道并且不在乎潜在的精度损失。
我们还可以使用static_cast来找回存在于void*指针中的值,如:
1 | void* p = &d; // 正确:任何非常量对象的地址都能存入void* |
但是在此过程中,我们必须严格保证转换后所得的类型就是指针所指的类型。类型一旦不符,会产生未定义的后果。
const_cast ——非常量对象解const的类型强转
const_cast只能改变运算对象底层const,如:
1 | const char *pc; |
对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质”。一旦我们去掉了某个对象的const性质,编译器将不会再阻止我们对改对象进行写操作。如果对象不是一个常量,使用强制类型转换获得写权限是合法的行为。否则,就会发生未定义的后果。
只有const_cast能改变表达式的常量属性,但是const_cast不能改变表达式的类型。
常用于函数重载的上下文
reinterpret_cast ——将内存中的值重新解释的类型强转
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。
1 | int *ip; |
原文链接: https://zijian.wang/2021/04/10/《C++ Primer 第五版》阅读过程查漏补缺 Chapter4/
版权声明: 转载请注明出处.